﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using NHibernate;
using DataModel;
using NHibernate.Cfg;
using NHibernate.Criterion;
using NHibernate.Tool.hbm2ddl;
using System.Diagnostics;
using NHibernate.LambdaExtensions;
using NHibernate.Transform;

namespace Tester
{
	class Program
	{
		static void Main(string[] args)
		{
			try
			{
				Configuration cfg = CreateConfiguration(typeof(Student).Assembly);
				
				using (ISessionFactory sessionFactory = cfg.BuildSessionFactory())
				using (ISession session = sessionFactory.OpenSession())
				{
					CreateDatabase(args, cfg, session);

					//Run(TheBasics, session);

					//Run(Relations_OneToMany, session);
					//Run(Relations_ManyToMany, session);

					//Run(Cascade_SaveUpdate, session);

					//Run(LazyLoading_OneEndedRelations, session);
					//Run(LazyLoading_OneEndedRelations_ForcingLoadViaCode, session);
					//Run(LazyLoading_ManyEndedRelations, session);
					//Run(LazyLoading_ManyEndedRelations_ForcingLoadViaCode, session);

					//Run(QueryingUsingCriteria_UniqueResult, session);
					//Run(QueryingUsingCriteria_SimpleList, session);
					//Run(QueryingUsingCriteria_ListWithRestrictions, session);
					//Run(QueryingUsingCriteria_WithNhLambdaExt, session);

					//Run(Querying_Projections, session);

					//Run(QueryingUsingCriteria_Aliases, session);
					//Run(QueryingUsingCriteria_Subcriteria, session);

					//QueryingUsingDetachedCriteria(session);
					//Querying_MultiCriteria_Paging(session);

					//Run(Querying_HQL, session);

					//Run(Caching_FirstLevelCache, session);
					//Run(Caching_FirstLevelCache_AfterCollectionLoad, session);
					//Run(Caching_FirstLevelCache_GetAndLoadDifferences, session);
					//UpdatingForeignKeysEfficientlyWithLoad(session);

					Run(Querying_FutureQueries, session);
				}
			}
			catch (Exception ex)
			{
				Console.Error.Write(ex);
			}
		}

		#region Basics

		private static void TheBasics(ISession session)
		{
			#region Load entity by PK
			
			const int classId = 1;
			Class existingClass = session.Get<Class>(classId);
			Console.WriteLine("Loaded class '{0}'", existingClass.Name);
			
			#endregion

			#region Updating entity

			existingClass.Description = "Physics for dummies";
			session.Update(existingClass);
			session.Flush();
			Console.WriteLine("Updated description of class '{0}'", existingClass.Name);

			#endregion

			#region Insert new entity

			Class newClass = new Class { Name = "Operating Systems" };
			session.Save(newClass);
			// session.Flush(); << Not needed because of PK type (already gone to DB)
			Console.WriteLine("Inserted new class '{0}'. ID={1}", newClass.Name, newClass.Id);

			#endregion
		}

		#endregion

		#region Relations

		private static void Relations_OneToMany(ISession session)
		{
			Professor prof = session.CreateCriteria<Professor>()
				.Add(Restrictions.Eq("Name", "Farnsworth"))
				.UniqueResult<Professor>();

			Console.WriteLine("Loaded professor '{0}'. Classes:", prof.Name);
			
			foreach (Class cls in prof.Classes)
				Console.WriteLine(" - {0}", cls.Name);
		}

		private static void Relations_ManyToMany(ISession session)
		{
			IList<Student> students = session.CreateCriteria<Student>()
				.List<Student>();

			foreach (Student student in students)
			{
				Console.WriteLine("Loaded student '{0}'. Classes:", student.Name);
				foreach (Class cls in student.Classes)
					Console.WriteLine(" - {0}", cls.Name);
			}
		}

		#endregion

		#region Cascades

		private static void Cascade_SaveUpdate(ISession session)
		{
			using (ITransaction txn = session.BeginTransaction( /* IsolationLevel */ ))
			{


				Professor prof = session.CreateCriteria<Professor>()
					.Add(Restrictions.Eq("Name", "Farnsworth"))
					.UniqueResult<Professor>();

				Class newClass = new Class {Name = "Robotics"};
				
				// NOTE:
				// Need to set both sides of bi-directional relation
				// (same for 1-* and *-*)
				newClass.Lecturer = prof;
				prof.Classes.Add(newClass);

				// Normally: prof.AddClass(newClass);

				session.Update(prof);
				// session.Save(newClass);  <<< no need to call



				txn.Commit();
			}
		}

		#endregion

		#region Querying

		#region ICriteria

		public static void QueryingUsingCriteria_UniqueResult(ISession session)
		{
			ICriteria criteria = session.CreateCriteria<Professor>()
				.Add(Restrictions.Eq("Name", "Farnsworth"));

			Professor prof = criteria
				.UniqueResult<Professor>();

			Console.WriteLine("Loaded professor '{0}' with ID={1}", prof.Name, prof.Id);
		}

		public static void QueryingUsingCriteria_SimpleList(ISession session)
		{
			ICriteria classCriteria = session.CreateCriteria<Class>();
			classCriteria.AddOrder(Order.Asc("Name"));

			IList<Class> classes = classCriteria.List<Class>();
			
			foreach (var c in classes)
			{
				Console.WriteLine("Loaded class '{0}'", c.Name);
			}
		}
		
		public static void QueryingUsingCriteria_ListWithRestrictions(ISession session)
		{
			ICriteria criteria = session.CreateCriteria<Student>()
				.Add(Restrictions.Eq("Gender", Gender.Female))
				.AddOrder(Order.Asc("Name"));

			IList<Student> femaleStudents = criteria.List<Student>();
			Console.WriteLine("Female students: ");
			foreach (var student in femaleStudents)
			{
				Console.WriteLine(" - " + student.Name);
			}

			/////////////////////////////////////////////

			IList<Professor> profsStartingWithFAndTeachingAtLeastOneClass = 
				session.CreateCriteria<Professor>()
				.Add(Restrictions.And(
					Restrictions.Like("Name", "F", MatchMode.Start).IgnoreCase(),
					Restrictions.Not(Restrictions.IsEmpty("Classes"))
				))
				.List<Professor>();

			Console.WriteLine("Profs starting with 'f' and teaching at least 1 class: ");
			foreach (Professor prof in profsStartingWithFAndTeachingAtLeastOneClass)
			{
				Console.WriteLine(" - " + prof.Name);
			}
		}

		#region ICriteria with nhlamdbaextensions

		public static void QueryingUsingCriteria_WithNhLambdaExt(ISession session)
		{
			string searchString = "al";

			ICriteria criteria = session.CreateCriteria<Student>()
				.Add<Student>(s => s.Gender == Gender.Female)
				.Add(SqlExpression.Like<Student>(s => s.Name, searchString, MatchMode.Anywhere))
				.AddOrder<Student>(s => s.Name, Order.Desc);

			IList<Student> femaleStudents = criteria.List<Student>();

			Console.WriteLine("Female students beginning with 'al' ordered by name");
			foreach (Student student in femaleStudents)
				Console.WriteLine(" - " + student.Name);
		}

		#endregion

		public static void QueryingUsingCriteria_Aliases(ISession session)
		{
			IList allStudentsInBiologyClass = session.CreateCriteria<Student>()
				.SetProjection(Projections.Property("Name"))
				.CreateAlias("Classes", "c")
				.Add(Restrictions.Eq("c.Name", "Biology"))
				.List();

			Console.WriteLine("All students in biology class: ");
			foreach (string studentName in allStudentsInBiologyClass)
				Console.WriteLine(" - " + studentName);

			// Note: 
			// contrived example! when you have bi-directional relation you would just 
			// load the Class entity and access the Students collection on it
			
			// ------------------

			// Note:
			// In case below, projecting 2 values, so you get back a result set where 
			// each item is itself an IList representing the row

			IList classCountByProfessor = session.CreateCriteria<Class>()
				.CreateAlias("Lecturer", "l")
				.SetProjection(Projections.ProjectionList()
					.Add(Projections.GroupProperty("l.Name"))
					.Add(Projections.RowCount())
				)
				.List();

			Console.WriteLine("Class counts by professor: ");
			foreach (IList row in classCountByProfessor)
			{
				Console.WriteLine(" - Professor '{0}' teaches {1} class(es)", row[0], row[1]);
			}
		}

		public static void QueryingUsingCriteria_Subcriteria(ISession session)
		{
			ICriteria studentCriteria = session.CreateCriteria<Student>()
				.SetProjection(Projections.Property("Name"));

			ICriteria classCriteria = studentCriteria.CreateCriteria("Classes");
			classCriteria
				.Add(Restrictions.Eq("Name", "Biology"));

			IList allStudentsInBiologyClass = studentCriteria.List();

			Console.WriteLine("All students in biology class: ");
			foreach (string studentName in allStudentsInBiologyClass)
				Console.WriteLine(" - " + studentName);
		}

		#endregion

		#region Projections

		public static void Querying_Projections(ISession session)
		{
			IList resultSet = session.CreateCriteria<Student>()
				.SetProjection(Projections.ProjectionList()
					.Add(Projections.Property("Name"))
					.Add(Projections.Property("HomeAddress.Line1"))
				)
				.AddOrder(Order.Asc("Name"))
				.List();

			Console.WriteLine("Student name and 1st address line: ");
			foreach (IList row in resultSet)
				Console.WriteLine(" - Student '{0}' lives in '{1}'", row[0], row[1]);
		}

		#endregion

		#region DetachedCriteria 

		public static void QueryingUsingDetachedCriteria(ISession session)
		{
			DetachedCriteria detachedCriteria = DetachedCriteria.For<Student>()
				.Add(Restrictions.Eq("Gender", Gender.Female))
				.AddOrder(Order.Asc("Name"));
				// ... etc

			// Only need the session here
			ICriteria exeutableCriteria = detachedCriteria.GetExecutableCriteria(session);
			IList<Student> femaleStudents = exeutableCriteria.List<Student>();
		}

		public static void Querying_MultiCriteria_Paging(ISession session)
		{
			const int TotalCount = 10;
			const int PageSize = 3;
			int totalPages = (int)Math.Ceiling((double)TotalCount / PageSize);

			InsertClasses(session, TotalCount);

			// Build up a normal detached criteria
			DetachedCriteria criteria = DetachedCriteria.For<Class>()
				.SetProjection(Projections.Property("Name"))
				.Add(Restrictions.Like("Name", "Cl", MatchMode.Start));
				// ... etc

			// Auto-create criteria to select total row count
			DetachedCriteria rowCountCriteria = CriteriaTransformer.TransformToRowCount(criteria);

			for (int i=0; i<totalPages; i++)
			{
				int firstResult = i * PageSize;

				criteria.SetFirstResult(firstResult); // 0, 3, 6 ...
				criteria.SetMaxResults(PageSize);

				// Run them both in the same trip to DB:
				IMultiCriteria multiCriteria = session.CreateMultiCriteria();
				multiCriteria.Add(criteria);
				multiCriteria.Add(rowCountCriteria);

				IList results = multiCriteria.List();
				IList pageResultSet = (IList)results[0];
				IList countResultSet = (IList)results[1];

				long totalCount = Convert.ToInt64(countResultSet[0]);

				DisplayPageData(i, totalPages, firstResult, pageResultSet, totalCount);
			}
		}

		public static void Querying_HQL(ISession session)
		{
			const string queryString1 = "from Class c left join fetch c.Lecturer";

			const string queryString2 =
				"select c.Name, p.Name " +
				"from Class as c " +
				"left join c.Lecturer as p " +
				"where p.Name like :name";

			// Run Query 1
			Console.WriteLine("Running HQL '{0}'", queryString1);

			IQuery q1 = session.CreateQuery(queryString1);
			foreach (Class cls in q1.List<Class>())
			{
				Console.WriteLine("Professor '{0}' teaches class '{1}'",
					cls.Lecturer.Name, cls.Name);
			}

			// Run Query 2
			Console.WriteLine();
			Console.WriteLine("Running HQL '{0}'", queryString2);

			IQuery q2 = session.CreateQuery(queryString2);
			q2.SetParameter("name", "f%");

			foreach (IList row in q2.List())
			{
				Console.WriteLine("Professor '{0}' teaches class '{1}'",
					row[1], row[0]);
			}
		}

		private static void DisplayPageData(int i, int totalPages, int firstResult, IList pageResultSet, long totalCount)
		{
			Console.WriteLine("--------------------");
			Console.WriteLine("[Page {0} / {1}] Showing items {2} to {3} of {4}:",
			                  i + 1, totalPages, 
			                  (firstResult + 1), firstResult + pageResultSet.Count, totalCount);

			foreach (string className in pageResultSet)
				Console.WriteLine(" - {0}", className);

			Console.WriteLine();
		}

		private static void InsertClasses(ISession session, int count)
		{
			using (ITransaction txn = session.BeginTransaction())
			{
				for (int i = 0; i < count; i++)
					session.Save(new Class {Name = "Class" + i});
				
				txn.Commit();
			}
			session.Clear(); // Clear 1st level cache
		}

		#endregion

		#endregion

		#region Caching

		public static void Caching_FirstLevelCache(ISession session)
		{
			Class cls = session.CreateCriteria<Class>().SetMaxResults(1).UniqueResult<Class>();
			Console.WriteLine("Loaded class '{0}' with ID={1}", cls.Name, cls.Id);

			Console.WriteLine("Loading class again using ID {0}...", cls.Id);
			Class reloadedCls = session.Get<Class>(cls.Id);
			Console.WriteLine("Reloaded class '{0}'", cls.Name);

			///////////////////////////////////////////////////////////////

			Console.WriteLine("Clearing first level cache");
			session.Clear();

			Console.WriteLine("Loading class again using ID {0}...", cls.Id);
			reloadedCls = session.Get<Class>(cls.Id);
			Console.WriteLine("Reloaded class '{0}'", cls.Name);
		}
		
		public static void Caching_FirstLevelCache_AfterCollectionLoad(ISession session)
		{
			IList<Class> allClasses = session.CreateCriteria<Class>().List<Class>();
			Console.WriteLine("Loaded all {0} classes", allClasses.Count);

			int id = allClasses[0].Id;

			Console.WriteLine("Loading class with ID={0}...", id);
			Class cls = session.Get<Class>(id);
			Console.WriteLine("...loaded class '{0}'", cls.Name);
		}

		public static void Caching_FirstLevelCache_GetAndLoadDifferences(ISession session)
		{
			const int classId = 1;

			Console.WriteLine("Getting class with ID={0} using Load...", classId);
			Class classGotUsingLoad = session.Load<Class>(classId);
			Console.WriteLine("...after call to Load", classId);

			Console.WriteLine("Class name is: {0}", classGotUsingLoad.Name);

			// notice the type of the instance!
			Console.WriteLine("Class instance type is {0}", classGotUsingLoad.GetType().Name);


			///////////////////////////////////////////////////////////////////////
			session.Clear(); // clear 1st level cache

			Console.WriteLine("Getting class with ID={0} using Get...", classId);
			Class classGotUsingGet = session.Get<Class>(classId);
			Console.WriteLine("...after call to Get", classId);

			Console.WriteLine("Class name is: {0}", classGotUsingGet.Name);
			Console.WriteLine("Class instance type is {0}", classGotUsingGet.GetType().Name);

			// Note:
			// Use Load when you already have an existing ID and you just 
			// want to update the FK of an entity
		}
		
		public static void UpdatingForeignKeysEfficientlyWithLoad(ISession session)
		{
			int profClumpId = GetProfessorId(session, "Clump");
			
			Class cls = GetClassByName(session, "Physics");

			Console.WriteLine("Updating class with prof ID={0}, setting prof ID={1}", 
				cls.Lecturer.Id, profClumpId);

			cls.Lecturer = session.Load<Professor>(profClumpId);
			session.Update(cls);
			session.Flush();
		}

		public static void Querying_FutureQueries(ISession session)
		{
			IEnumerable<Class> futureClasses = 
				session.CreateCriteria<Class>()
				.Future<Class>();  // .List<Class>();

			IEnumerable<Professor> futureProfs =
				session.CreateCriteria<Professor>()
				.Future<Professor>();  // .List<Class>();

			IFutureValue<long> futureCount = session.CreateCriteria<Student>()
				.SetProjection(Projections.RowCountInt64())
				.FutureValue<long>();

			///////////////////////////////////////////////

			Console.WriteLine("All classes:");
			foreach (Class cls in futureClasses)
				Console.WriteLine(" - {0}", cls.Name);

			Console.WriteLine();
			Console.WriteLine("All profs:");
			foreach (Professor p in futureProfs)
				Console.WriteLine(" - {0}", p.Name);

			Console.WriteLine();
			Console.WriteLine("Count of students is: {0}", futureCount.Value);
		}

		private static Class GetClassByName(ISession session, string className)
		{
			return session.CreateCriteria<Class>()
				.Add<Class>(c => c.Name == className)
				.UniqueResult<Class>();
		}

		private static int GetProfessorId(ISession session, string profName)
		{
			return session.CreateCriteria<Professor>()
				.SetProjection(Projections.Id())
				.Add<Professor>(p => p.Name == profName)
				.UniqueResult<int>();
		}

		#endregion

		#region Lazy Loading

		public static void LazyLoading_OneEndedRelations(ISession session)
		{
			ICriteria classCriteria = session.CreateCriteria<Class>();
			IList<Class> classes = classCriteria.List<Class>();

			foreach (var c in classes)
			{
				Console.WriteLine("Class '{0}' is taught by '{1}'",
					c.Name,
					c.Lecturer.Name);
			}
			
			/* NOTE:
			 * Prof is lazy loaded by default. To force a load by default, in Class.hbm.xml change: 
			 * <many-to-one name="Lecturer" column="ProfessorId" />  
			 * to: 
			 * <many-to-one name="Lecturer" column="ProfessorId" lazy="false" />
			 * 
			 * But default fetch strategy in 'select', so you need:
			 * <many-to-one name="Lecturer" column="ProfessorId" lazy="false" fetch="join" />
			 * 
			*/
		}

		public static void LazyLoading_OneEndedRelations_ForcingLoadViaCode(ISession session)
		{
			ICriteria classCriteria = session.CreateCriteria<Class>();
			IList<Class> classes = classCriteria
				.SetFetchMode("Lecturer", FetchMode.Eager)	 //.SetFetchMode<Class>(c => c.Lecturer, FetchMode.Eager)
				.List<Class>();

			foreach (var c in classes)
			{
				Console.WriteLine("Class '{0}' is taught by '{1}'",
					c.Name,
					c.Lecturer.Name);
			}
		}

		public static void LazyLoading_ManyEndedRelations(ISession session)
		{
			ICriteria criteria = session.CreateCriteria<Class>();
			IList<Class> classes = criteria.List<Class>();

			foreach (var cls in classes)
			{
				Console.WriteLine("Class '{0}' has students:", cls.Name);
				foreach (Student student in cls.Students)
				{
					Console.WriteLine(" - '{0}'", student.Name);	
				}
			}

			/*
			 * If you really want to load all students all the time use:
			 
			 <bag name="Students" table="Student_Class" lazy="false" fetch="join" >
				...
			 
			 */
		}

		public static void LazyLoading_ManyEndedRelations_ForcingLoadViaCode(ISession session)
		{
			ICriteria criteria = session.CreateCriteria<Class>()
				.SetFetchMode<Class>(c => c.Students, FetchMode.Eager);

			IList<Class> classes = criteria.List<Class>();

			foreach (var cls in classes)
			{
				Console.WriteLine("Class '{0}' has students:", cls.Name);
				foreach (Student student in cls.Students)
				{
					Console.WriteLine(" - '{0}'", student.Name);
				}
			}
		}

		#endregion

		#region Setup

		private static Configuration CreateConfiguration(Assembly assembly)
		{
			Configuration cfg = new Configuration();
			cfg.AddAssembly(assembly);
			cfg.Configure();
			return cfg;
		}

		private static void CreateDatabase(string[] args, Configuration cfg, ISession session)
		{
			bool createDb = args.Length > 0 && args[0] == "createdb";
			if (createDb)
			{
				CreateDb(cfg);
				PopulateSampleData(session);

				session.Clear(); // Have to clear out 1st level cache 
				// to show data access
			}
		}

		private static void PopulateSampleData(ISession session)
		{
			var alice = new Student
			{
				Name = "Alice",
				DateOfBirth = DateTime.Now.AddYears(-20),
				Gender = Gender.Female,
				HomeAddress = new Address { Line1 = "Wonderland" }
			};

			var bob = new Student
			{
				Name = "Bob",
				DateOfBirth = DateTime.Now.AddYears(-30),
				Gender = Gender.Male,
				HomeAddress = new Address { Line1 = "Springfield" }
			};

			/////////////////////////////////////////////////////////

			var prof1 = new Professor { Name = "Farnsworth" };
			var prof2 = new Professor { Name = "Clump" };

			/////////////////////////////////////////////////////////

			var class1 = new Class { Name = "Physics" };
			var class2 = new Class { Name = "Philosophy" };
			var class3 = new Class { Name = "Biology" };

			/////////////////////////////////////////////////////////

			class1.Students.Add(alice);
			alice.Classes.Add(class1);

			class2.Students.Add(bob);
			bob.Classes.Add(class2);

			class3.Students.Add(bob);
			bob.Classes.Add(class3);

			class1.Lecturer = prof1;
			prof1.Classes.Add(class1);

			class2.Lecturer = prof1;
			prof1.Classes.Add(class2);

			class3.Lecturer = prof2;
			prof2.Classes.Add(class3);

			session.Save(class1);
			session.Save(class3);

			session.Flush();
		}

		private static void CreateDb(Configuration cfg)
		{
			Console.WriteLine("-----------------------------------------");
			Console.WriteLine("Creating database from NHibernate config");
			var export = new SchemaExport(cfg);
			export.Create(true, true);
		}

		[DebuggerStepThrough]
		private static void Run(Action<ISession> exampleToRun, ISession session)
		{
			Console.WriteLine();
			Console.WriteLine("-----------------------------------------");
			Console.WriteLine("Running: {0}", exampleToRun.Method.Name);
			Console.WriteLine();

			session.Clear();

			exampleToRun(session);
		}

		#endregion
	}
}
